package cz.cuni.amis.pogamut.episodic.episodes;

import cz.cuni.amis.pogamut.episodic.memory.Parameters;
import cz.cuni.amis.pogamut.episodic.schemas.SchemaObjectNode;
import cz.cuni.amis.pogamut.episodic.schemas.SchemaSlot;
import cz.cuni.amis.pogamut.episodic.schemas.SlotContent;
import java.io.Serializable;
import java.lang.Double;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * As episode tree can be imagined as experienced instance of the decision
 * tree, <code>ObjectSlot</code> can is an experienced equivalent of
 * <code>AffordanceSlot</code>. Of course several <code>ObjectSlot</code>s can
 * be associated with one <code>AffordanceSlot</code>. <code>ObjectSlot</code>s
 * can be attached to any action or intention episode node (same as affordance
 * slots can be attached to decision nodes). It represent the object being used
 * while performing specified action. In order for action to be completed
 * successfully all slots have to be filled by particular items first. However
 * if no such object was found, the slot still exists in memory, it just isn't
 * linked with any object. Slots are the only was how to remember that
 * an object was used to perform an action.
 *
 * @author Michal Cermak
 */
public class ObjectSlot implements Serializable {
    /**
     * Determines if a de-serialized file is compatible with this class.
     *
     * Maintainers must change this value if and only if the new version
     * of this class is not compatible with old versions. See Sun docs
     * for <a href=http://java.sun.com/products/jdk/1.1/docs/guide
     * /serialization/spec/version.doc.html> details. </a>
     *
     * Not necessary to include in first version of the class, but
     * included here as a reminder of its importance.
     */
    private static final long serialVersionUID = 1L;

    /**
     * ID of this node. Used as an ID of vertex representing
     * this node when visualizing Chronobag View.
     */
    private final int id;

    /**
     * Type of an object that can fill this slot. Each of the slots attached
     * to an episodic node has to have a unique type.
     */
    private final String type;

    /**
     * Reference to <code>EpisodeNode</code> this slot is attached to.
     */
    private final EpisodeNode parentNode;

    /**
     * Map of <code>ObjectNode</code>s representing objects that satisfy this
     * affordance. Usually there will be just one object, but it is possible
     * to have several objects of the same type in one slot.
     *
     * Each connection to object node is assigned a double score (hence a map).
     * Scores is used to determine if the link between the episode node
     * and the object node can be forgotten.
     */
    private HashMap<ObjectNode, Double> objects = new HashMap<ObjectNode, Double>();

    /**
     * Instantiate the slot class by providing unique ID that
     * will be used for visualizing purposes, type of objects that are needed
     * to fill this slot and reference to the node, this slot is attached to.
     *
     * @param   _id Unique ID of this node.
     * @param   _type   Type of the slot. No two slots of the same type can be
     * attached to the one episodic node.
     * @param   parent  Reference  to the node, this slot is attached to.
     */
    public ObjectSlot(int _id, String _type, EpisodeNode parent) {
        id = _id;
        type = _type;
        parentNode = parent;
    }

    /**
     * Getter method for the <code>id</code> variable.
     *
     * @return  Returns ID of this node. Used as an ID of vertex representing
     * this node when visualizing Chronobag View.
     */
    public int getId() {
        return id;
    }

    /**
     * Getter method for the <code>type</code> variable.
     *
     * @return  Returns type of an object that can fill this slot.
     * Each of the slots attached to an episodic node has to have a unique type.
     */
    public String getType() {
        return type;
    }

    /**
     * Getter method for the <code>parentNode</code> variable.
     *
     * @return  Returns s reference to <code>EpisodeNode</code> this slot
     * is attached to.
     */
    public EpisodeNode getParentNode() {
        return parentNode;
    }

    /**
     * This method can be to unlink all <code>ObjectNode</code>s that were
     * previously linked with this slot. After calling this method it is
     * ensured that the slot will not be connected with any object node.
     * It will not be possible to recover information 'what object was used
     * to perform the parent episodic node' from memory.
     * <p>
     * Can be used to simulate the forgetting process.
     */
    public void emptySlot() {
        int count = objects.size();
        EpisodeNode n = parentNode;
        while (n != null) {
            n.numberOfSubNodesWithObjects -= count;
            n = n.getParent();
        }

        for (ObjectNode o : objects.keySet()) {
            o.usedAt.remove(this);
        }

        parentNode.getEpisodeRoot().getParentChronobag().numberOfUsedObjects -= count;
        if (parentNode.getEpisodeRoot().getParentChronobag().numberOfUsedObjects < 0)
            parentNode.getEpisodeRoot().getParentChronobag().numberOfUsedObjects = 0;
//TODO: check this
//        assert parentNode.getEpisodeRoot().getParentChronobag().numberOfUsedObjects >= 0;


        objects.clear();
    }

    public void remove(ObjectNode o) {
        objects.remove(o);
        o.usedAt.remove(this);

        parentNode.getEpisodeRoot().getParentChronobag().numberOfUsedObjects--;
        assert parentNode.getEpisodeRoot().getParentChronobag().numberOfUsedObjects >= 0;

        EpisodeNode n = parentNode;
        while (n != null) {
            n.numberOfSubNodesWithObjects -= 1;
            n = n.getParent();
        }

        if (objects.isEmpty()) {
            deleteSlot();
        }
    }

    /**
     * Links an this slot with given <code>ObjectNode</code> representing
     * and object. In agent's memory this object will be remembered as
     * item that was used to perform the episodic node this slot is attached to.
     *
     * @param obj   Reference to an <code>ObjectNode</code> that is to be linked
     * with this slot.
     * @return  Returns true if new object was linked with the slot. Returns
     * false if the object was already linked with the slot and no new connection
     * was created.
     */
    public boolean addObject(ObjectNode obj, boolean increaseCounter, boolean temporary) {
        if (objects.keySet().contains(obj)) return false;
        objects.put(obj, parentNode.getEpisodeRoot().mem.parameters.MAX_OBJECT_SCORE);
        if (!temporary)
            obj.usedAt.add(this);

        if (parentNode.getEpisodeRoot().getParentChronobag() != null) {
            parentNode.getEpisodeRoot().getParentChronobag().numberOfUsedObjects++;
        }

        if (!increaseCounter) return true;
        EpisodeNode node = parentNode;
        while (node != null) {
            node.numberOfSubNodesWithObjects++;
            node = node.getParent();
        }

        return true;
    }

    // Shorthand for last arg false
    public boolean addObject(ObjectNode obj, boolean increaseCounter) {
        return addObject(obj, increaseCounter, false);
    }

    /**
     * A getter method for the <code>objects</code> variable.
     *
     * @return  Returns collection of <code>ObjectNode</code>s representing objects
     * that satisfy this affordance. Usually there will be just one object,
     * but it is possible to have several objects of the same type in one slot.
     * According to agent's memories these objects were used to perform
     * the episodic node this slot is attached to.
     */
    public Collection<ObjectNode> getUsedObjects() {
        return objects.keySet();
    }
    /**
     * Getter method for the <code>objects.values()</code> (scores) variable.
     *
     * @return Returns Score of a slot. Used to determine if the link
     * between the episode node and the object node can be forgotten.
     */
    public Collection<Double> getScore() {
        return objects.values();
    }

    /**
     * Method used to compute the new score of a slot. Used to determine if
     * the link between the episode node and the object node can be forgotten.
     *
     * @return  Returns the new score value of this slot.
     */
    public Collection<Double> computeScore() {
        if (type.equals("Other")) {
            for (Map.Entry<ObjectNode, Double> entry : objects.entrySet()) {
                entry.setValue(parentNode.getEpisodeRoot().mem.parameters.DEFAULT_OBJECT_SCORE);
            }
            return objects.values();
        }
        if (parentNode.associatedNode == null) {
            for (Map.Entry<ObjectNode, Double> entry : objects.entrySet()) {
                entry.setValue(0.0);
            }
            return objects.values();
        }
        if (objects.isEmpty()) {
            return objects.values();
        }
        SchemaSlot ss = parentNode.associatedNode.getAssociatedNode().getSlot(type);
        SchemaObjectNode so;
        SlotContent c;
        //we will count both: slot implies object and object implies slot
        int count, countAllObject, countAllSlot;
        countAllSlot = 0;
        for (SlotContent con : ss.getSlotContents()) {
            countAllSlot += con.getCounts().get(0).iterator().next().getCount();
        }

        double score;
        for (ObjectNode o : objects.keySet()) {
            count = 0;
            c = ss.getSlotContent(o.getName());
            count += c.getCounts().get(0).iterator().next().getCount();

            countAllObject = 0;
            so = c.getObject();
            for (SlotContent con : so.getSlotContents()) {
                countAllObject += con.getCounts().get(0).iterator().next().getCount();
            }

            //score 1 for rare connections, score 0 for 100% derivable connections
            //actually it is not 1 and 0 but MAX_OBJECT_SCORE and MIN_OBJECT_SCORE
            score = 1.0 - 2.0*count/(countAllObject + countAllSlot);
            score = (parentNode.getEpisodeRoot().mem.parameters.MAX_OBJECT_SCORE - parentNode.getEpisodeRoot().mem.parameters.MIN_OBJECT_SCORE) * score
                    + parentNode.getEpisodeRoot().mem.parameters.MIN_OBJECT_SCORE;
            objects.put(o, score);
        }

        return objects.values();
    }

    /**
     * Deletes the slot. Before the slot is unlinked from all the object nodes
     * and parent episode node, all its filling objects are moved to "Other"
     * type slot on parent node.
     *
     * @return  Returns true.
     */
    public boolean deleteSlot() {
        String otherType = "Other";
        ObjectSlot otherSlot = getParentNode().getObjectSlot(otherType);
        if (otherSlot == null) {
            getParentNode().addSlot(otherType);
            otherSlot = getParentNode().getObjectSlot(otherType);
        }
        for (ObjectNode o : getUsedObjects()) {
            //if object already existed in "other" slot, nothing happened - it was not relocated
            otherSlot.addObject(o, true);
        }
        emptySlot();
        getParentNode().slots.remove(type);
        return true;
    }

    public void forgetConnections(double kScore) {
        // avoids a ConcurrentModificationException
        Set<ObjectNode> set = new HashSet<ObjectNode>();
        set.addAll(objects.keySet());
        for (ObjectNode obj : set) {
            if (objects.get(obj) < kScore) {
                remove(obj);
            }
        }
    }

    /**
     * Returns score of connection with particular object.
     *
     * @param o ObjectNode we are querying for.
     * @return  Returns the score of object connection with this slot. Returns
     * zero if the slot is not connected to the parameter node.
     */
    public double getScore(ObjectNode o) {
        if (objects.containsKey(o)) {
            return objects.get(o);
        }
        return 0;
    }
}
